/*
 * Copyright 2018 dc-square GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.hivemq.extensions.rbac.configuration;

import com.hivemq.extension.sdk.api.annotations.NotNull;
import com.hivemq.extension.sdk.api.annotations.Nullable;
import com.hivemq.extension.sdk.api.annotations.ThreadSafe;
import com.hivemq.extensions.rbac.configuration.entities.FileAuthConfig;
import com.hivemq.extensions.rbac.configuration.entities.ExtensionConfig;
import com.hivemq.extensions.rbac.utils.TimeConvert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static java.util.Collections.unmodifiableList;


/**
 * credentials配置
 */
@ThreadSafe
public class Configuration {

    /** 权限配置文件 */
    static final String CONFIG_NAME = "credentials.xml";
    private static final Logger log = LoggerFactory.getLogger(Configuration.class);

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    /** COWAL 在这里是完美的，因为回调函数不会定期变化.
     *  回调在该类和可重新加载的配置任务之间共享。只能通过这个类进行修改
     */
    private final List<ReloadCallback> callbacks = new CopyOnWriteArrayList<>();

    /** 个队列是线程安全的，并且在这个类和可重载的配置文件任务之间共享 */
    private final Queue<ReloadCallback> newCallbacks = new LinkedBlockingQueue<>();
    private final @NotNull File extensionHomeFolder;
    private final @NotNull ConfigParser configParser;

    // 守卫的锁
    private @Nullable FileAuthConfig config;


    public Configuration(@NotNull final File extensionHomeFolder,
                         @NotNull final ScheduledExecutorService extensionExecutorService,
                         @NotNull final ExtensionConfig extensionConfig) {

        configParser = new ConfigParser(extensionConfig);
        this.extensionHomeFolder = extensionHomeFolder;
        final ReloadConfigFileTask reloadableTask = new ReloadConfigFileTask(extensionHomeFolder,
                extensionExecutorService,
                /* 我们不希望任务修改回调! */
                unmodifiableList(callbacks),
                newCallbacks,
                configParser,
                new ConfigArchiver(extensionHomeFolder, new XmlParser()), this);
        extensionExecutorService.scheduleWithFixedDelay(reloadableTask, extensionConfig.getReloadInterval(),
                extensionConfig.getReloadInterval(), TimeUnit.SECONDS);
    }

    public void init() {

        config = configParser.read(getConfigFile(extensionHomeFolder));

        if (config == null) {
            log.warn("没有文件认证扩展的凭据配置文件可用，拒绝所有连接.");
        }

        addReloadCallback(new ReloadCallback() {
            @Override
            public void onReload(@Nullable final FileAuthConfig oldConfig, @NotNull final FileAuthConfig newConfig) {
                final Lock writeLock = lock.writeLock();
                writeLock.lock();
                try {
                    config = newConfig;
                } finally {
                    writeLock.unlock();
                }
            }
        });
    }

    @Nullable
    public FileAuthConfig getCurrentConfig() {
        final Lock readLock = lock.readLock();
        readLock.lock();
        try {
            return config;
        } finally {
            readLock.unlock();
        }
    }

    /**
     * 添加一个重新加载回调.
     */
    public void addReloadCallback(@NotNull final ReloadCallback callback) {
        callbacks.add(callback);
        newCallbacks.add(callback);
    }


    /**
     * 每次配置文件更改时都会触发的回调.
     * <p>
     * 回调保证不会并发执行.
     * <p>
     * 所有回调都在专用的执行器中执行。回调不是并发执行的，所以要确保它们不会阻塞太长时间
     */
    public interface ReloadCallback {

        /**
         * 当配置更改时调用的回调
         *
         * @param oldConfig the old config
         * @param newConfig the new config
         */
        void onReload(@Nullable FileAuthConfig oldConfig, @NotNull FileAuthConfig newConfig);

    }

    /** 加载配置文件的任务 */
    private static class ReloadConfigFileTask implements Runnable {

        private final @NotNull ExecutorService callbackExecutor;
        private final @NotNull ConfigArchiver configArchiver;
        private final @NotNull ConfigParser configParser;
        private final @NotNull File configFile;
        private final Configuration configuration;

        private final @NotNull List<ReloadCallback> callbacks;
        private final @NotNull Queue<ReloadCallback> newCallbacks;
        private @Nullable FileAuthConfig oldConfig;
        private long lastReadTimestamp = 0;

        ReloadConfigFileTask(@NotNull final File extensionHomeFolder,
                             @NotNull final ExecutorService callbackExecutor,
                             @NotNull final List<ReloadCallback> callbacks,
                             @NotNull final Queue<ReloadCallback> newCallbacks,
                             @NotNull final ConfigParser configParser,
                             @NotNull final ConfigArchiver configArchiver,
                             @NotNull final Configuration configuration) {

            this.callbacks = callbacks;
            this.newCallbacks = newCallbacks;
            this.configParser = configParser;
            this.callbackExecutor = callbackExecutor;
            this.configArchiver = configArchiver;

            configFile = getConfigFile(extensionHomeFolder);
            this.configuration = configuration;
            lastReadTimestamp = System.currentTimeMillis();
            oldConfig = configParser.read(configFile);
        }

        @Override
        public void run() {

            if (!configFile.exists()) {
                log.debug("没有文件认证扩展{}可用的凭证文件，暂时不重新加载配置", configFile.getAbsolutePath());
                return;
            }

            // 最后修改时间
            final long lastModification = configFile.lastModified();
            if (configuration.getCurrentConfig() != null && lastReadTimestamp >= lastModification) {
                log.trace("配置文件未更新{}. 上次载入时间:{}", configFile.getAbsolutePath(),
                        TimeConvert.toString(lastModification, "yyyy-MM-dd HH:mm:ss"));
                return;
            }

            log.debug("认证凭证更新, 重新载入{}.", configFile.getAbsolutePath());
            final FileAuthConfig newConfig = configParser.read(configFile);

            lastReadTimestamp = System.currentTimeMillis();

            if (newConfig == null) {
                // 没有更改或无效的新配置
                return;
            }

            log.info("更改文件认证扩展的凭据配置，使用新的配置.");

            // 备份旧的配置存档
            try {
                configArchiver.archive(oldConfig);
            } catch (IOException e) {
                log.warn("旧凭证配置的归档失败。原因: {}", e.getMessage());
            }

            oldConfig = newConfig;

            for (ReloadCallback callback : callbacks) {
                callback.onReload(oldConfig, newConfig);
            }
        }
    }

    @NotNull
    private static File getConfigFile(final @NotNull File extensionHomeFolder) {
        return new File(extensionHomeFolder, CONFIG_NAME);
    }

}
